package edu.uclmgmu.bacteriosec.domain;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.Random;
import java.util.Vector;

import edu.uclmgmu.bacteriosec.aspects.common.FilesStore;
import edu.uclmgmu.bacteriosec.aspects.common.Store;
import edu.uclmgmu.bacteriosec.domain.data.AspectFamily;
import edu.uclmgmu.bacteriosec.domain.data.CallResult;
import edu.uclmgmu.bacteriosec.domain.data.ListOfAspectsFamilies;
import edu.uclmgmu.bacteriosec.domain.data.MethodCatched;
import edu.uclmgmu.bacteriosec.domain.data.TestCaseExecuted;
import edu.uclmgmu.bacteriosec.gui.JDContinue;
import edu.uclmgmu.bacteriosec.gui.JFBacterioSecMain;
import edu.uclmgmu.bacteriosec.rmiserver.Listener;

public class Controller {
	public static final int LOG_WITH_FILES = 0;
	public static final int LOG_WITH_RMI = 1;
	public static final int ALLEGRO = 0;
	public static final int ALLEGRO_MA_NON_TROPPO = 1;
	
	private JFBacterioSecMain ventana;
	private String resultsPath;
	private boolean executeOnJUnit;
	private int executionMode = 0;
	private ListOfAspectsFamilies aspectsFamiliesUsed=new ListOfAspectsFamilies();
	private Vector<String> joinPointsMatched=new Vector<String>();
	private String aspectedOperatorsPath;
	private long meanTimeInMilliseconds;
	private Vector<String> alreadyAddeds=new Vector<String>();
	private Listener listener;
	private int probability;
	private String workingPath;
	private String aspectJRT;
	private String classPath;
	private String jUnitJarPath;
	private String jUnitFile;
	private String mainClass;
	private String bootPath;
	private String aspectJWeaver;
	private int logWith;
	private int verbosityLevel;

	public Controller(JFBacterioSecMain ventana) {
		this.ventana=ventana;
	}
	
	public void setVerbosityLevel(int verbosityLevel) {
		this.verbosityLevel=verbosityLevel;
	}
	
	public void setProbability(int probability) {
		this.probability=probability;
	}
	
	public void setExecuteOnJUnit(boolean value) {
		this.executeOnJUnit=value;
	}
	
	public void setWorkingPath(String workingPath) {
		this.workingPath=workingPath;
	}
	
	public void setAspectedOperatorsPath(String aspectedOperatorsPath) {
		this.aspectedOperatorsPath = aspectedOperatorsPath;
	}

	public void setAspectJRT(String aspectJRT) {
		this.aspectJRT = aspectJRT;
	}

	public void setClassPath(String classPath) {
		this.classPath=classPath;
	}
	
	public void setJUnitJarPath(String jUnitJarPath) {
		this.jUnitJarPath=jUnitJarPath;
	}
	
	public void setJUnitFile(String jUnitFile) {
		this.jUnitFile=jUnitFile;
	}
	
	public void setMainClass(String mainClass) {
		this.mainClass=mainClass;
	}
	
	public void setBootPath(String bootPath) {
		this.bootPath=bootPath;
	}
	
	public void setAspectJWeaver(String aspectJWeaver) {
		this.aspectJWeaver=aspectJWeaver;
	}
	
	public void setLogWith(int logWith) {
		this.logWith=logWith;
		if (logWith==Controller.LOG_WITH_FILES) {
			Store.setHowToLog(Controller.LOG_WITH_FILES);
		} else {
			Store.setHowToLog(Controller.LOG_WITH_RMI);
		}
	}
	
	protected void execute() {	
		try {
			java.util.Random dado=new Random(); 
			int executions=0;
			int not=this.aspectsFamiliesUsed.getTotalNumberOfCalls();
			ventana.setNumberOfTasks(not);
			
			Enumeration<AspectFamily> aspectsFamilies=this.aspectsFamiliesUsed.getFamilies();
			while (aspectsFamilies.hasMoreElements()) {
				AspectFamily aspectFamily=aspectsFamilies.nextElement();
				ventana.log("Start of applying family " + aspectFamily.getName());

				Enumeration<MethodCatched> mmcc=aspectFamily.getMethodsCatched();
				while (mmcc.hasMoreElements()) {
					MethodCatched mc=mmcc.nextElement();
					ventana.log("\tStart of catching " + mc.getName());
					Vector<TestCaseExecuted> ttccee = mc.getTestCases();
					for (int i=0; i<ttccee.size(); i++) {
						TestCaseExecuted tce=ttccee.get(i);
						int noc=tce.getNumberOfCalls();
						ventana.log("\t\tStart of " + tce.getName() + " (" + noc + " calls)");
						for (int j=1; j<=noc; j++) {
							double tirada=this.executionMode==JFBacterioSecMain.ONLY_ORIGINAL ? 1 : dado.nextDouble()*100;
							if (tirada<probability) {
								ventana.log("\t\t\tStart call " + j);
								FilesStore.createFileForCallInTurn(mc.getName(), tce.getName(), j);
								execute(aspectedOperatorsPath, aspectFamily, tce, mc, j);
								ventana.log("\t\t\tEnd call " + j);
							}
							ventana.updateProgressBarTask();
						}
						ventana.log("\t\tEnd of " + tce.getName());
					}
				}
				ventana.log("End of applying family " + aspectFamily.getName());
			}
			ventana.setNumberOfExecutionsText(executions + " executions");
			ventana.log(this.aspectsFamiliesUsed);
		} catch (Exception e) {
			ventana.log(e.toString());
			return;
		}
	}

	public void run() {		
		Runnable r=new Runnable() {
			int currentActivity=0;
			
			void updateProgressBarActivity() {
				ventana.setCurrentActivity(++currentActivity);
			}
			
			void setNoOfActivities(int max) {
				ventana.setNumberOfActivities(max);
			}
			
			@Override
			public void run() {
				resultsPath=null;
				if (ventana.getExecuteOriginal()) {
					executionMode=JFBacterioSecMain.ONLY_ORIGINAL;
					setNoOfActivities(2);
					ventana.setCurrentActivityText("1/2: Setting up");
					setUp();
					updateProgressBarActivity();
					ventana.setCurrentActivityText("2/2: Executing original");
					executeOriginal();
					updateProgressBarActivity();
					return;
				}
				if (ventana.getOnlyCountCalls()) {
					executionMode=JFBacterioSecMain.ONLY_COUNT;
					setNoOfActivities(2);
					ventana.setCurrentActivityText("1/2: Setting up");
					setUp();
					updateProgressBarActivity();
					ventana.setCurrentActivityText("2/2: Counting calls");
					openListener();
					countCalls();
					closeListener();
					updateProgressBarActivity();
					return;
				} 	
				if (ventana.getFullExecution()) {
					createResultsDirectory();
					setNoOfActivities(4);
					ventana.setCurrentActivityText("1/4: Setting up");
					setUp();
					updateProgressBarActivity();
					ventana.log("STEP 1.- EXECUTING ORIGINAL");
					ventana.log("===========================");
					executionMode=JFBacterioSecMain.ONLY_ORIGINAL;
					ventana.setCurrentActivityText("2/4: Executing original");
					executeOriginal();
					updateProgressBarActivity();
					ventana.log("STEP 2.- COUNTING CALLS");
					ventana.log("=======================");
					executionMode=JFBacterioSecMain.ONLY_COUNT;
					ventana.setCurrentActivityText("3/4: Counting calls");
					openListener();
					countCalls();
					updateProgressBarActivity();
					JDContinue jd=new JDContinue();
					jd.setModal(true);
					jd.setVisible(true);
					return;
				}
				if (ventana.getMakeATrace()) {
					executionMode=JFBacterioSecMain.MAKE_TRACE;
					setNoOfActivities(2);
					ventana.setCurrentActivityText("1/2: Setting up");
					setUp();
					updateProgressBarActivity();
					ventana.setCurrentActivityText("2/2: Tracing");
					makeATrace();
					updateProgressBarActivity();
					return;
				}		
			}
		};
		Thread t=new Thread(r);
		t.start();
	}
	
	public void continueFullExecution() {
		Runnable r=new Runnable() {
			public void run() {
				ventana.log("\nSTEP 3.- EXECUTING ASPECTS");
				ventana.log("==========================");
				executionMode=JFBacterioSecMain.FULL;
				ventana.setCurrentActivityText("4/4: Executing aspects");
				ventana.setCurrentTaskText("");
				ventana.setCurrentTask(0);
				execute();
				closeListener();
				ventana.setNumberOfActivities(4);
			}
		};
		Thread t=new Thread(r);
		t.start();
	}
	
	protected void closeListener() {
		if (logWith==Controller.LOG_WITH_RMI) {
			try {
				listener.stop();
			} catch (RemoteException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (MalformedURLException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (NotBoundException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	protected void openListener() {
		if (logWith==Controller.LOG_WITH_RMI) {
			try {
				if (listener==null)
					listener=new Listener();
				listener.start();
			} catch (RemoteException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			} catch (MalformedURLException e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}
		}
	}

	protected void executeOriginal() {
		try {
			execute(aspectedOperatorsPath, null, null, null, 0);
		} catch (IOException e) {
			ventana.log(e.toString());
			//tReloj.interrupt();
			return;
		}
		
	}

	protected synchronized void execute(String aspectedOperatorsPath, AspectFamily aspectFamily, TestCaseExecuted testCase, MethodCatched catchedMethod, int callNumber) throws IOException {
		String testCaseName=null;
		if (testCase!=null)	
			testCaseName=testCase.getName();
		String catchedMethodName=null;
		if (catchedMethod!=null) 
			catchedMethodName=catchedMethod.getName();
		String aspectFamilyName=null;
		if (aspectFamily!=null)
			aspectFamilyName=aspectFamily.getName();
		Vector<String> aspectsClassFiles = loadAspectsToExecute(aspectedOperatorsPath, aspectFamilyName);
		for (String aspectClassFile : aspectsClassFiles) {			
			aspectClassFile = updateAOPFile(aspectClassFile);
			String currentClassPath = loadCurrentClassPath();
			String launchingClass = loadLaunchingClass(testCaseName);
			long timeOut=((this.executionMode==JFBacterioSecMain.ONLY_COUNT || this.executionMode==JFBacterioSecMain.ONLY_ORIGINAL) ? -1 : this.meanTimeInMilliseconds);
			String aspectsWeaver=this.executionMode==JFBacterioSecMain.ONLY_ORIGINAL ? null : this.aspectJWeaver;
			if (this.executionMode==JFBacterioSecMain.FULL) {
				try {
					FilesStore.createFileForCurrentCall(catchedMethodName, testCaseName, 1);
					ventana.log("\t\t\t\tCreated file " + System.getProperty("java.io.tmpdir") + "currentCall.txt");
				}
				catch (Exception e) {
					ventana.log("\t\t\t\tError creating " + catchedMethodName + ".numberOfCurrentCall");
				}
			}
			Executor e=new Executor(ventana, aspectsWeaver, currentClassPath, launchingClass, this.bootPath, this.workingPath, timeOut);
			String[] salida={""};
			e.setOutputText(salida);
			e.setUsingJUnit(this.executeOnJUnit);
			e.run();
			
			int rt=e.getReturnValue();
			String result="";
			if (rt==-1000 || rt==-2000) {
				CallResult cr=new CallResult(callNumber, aspectClassFile, catchedMethod, testCase);
				cr.processResult(".E" + salida[0]);
				result=cr.getResult();
				catchedMethod.add(cr);
				aspectFamily.addCallResult(cr);
			} else if (testCase!=null) {
				CallResult cr=new CallResult(callNumber, aspectClassFile, catchedMethod, testCase);
				cr.processResult(salida[0]);
				result=cr.getResult();
				catchedMethod.add(cr);
				aspectFamily.addCallResult(cr);
			}
			if (this.verbosityLevel==Controller.ALLEGRO) {
				ventana.log(aspectClassFile, rt, salida[0]);
			} else {
				ventana.log(aspectClassFile, rt, result);
			}
			this.joinPointsMatched.clear();

			// If we're counting calls, let's check whether this aspect is or not executed
			if (this.executionMode==JFBacterioSecMain.ONLY_COUNT) {
				String currentAspectFamily=aspectClassFile.substring(0, aspectClassFile.lastIndexOf("/"));
				AspectFamily aspectFamilyAuxi=this.aspectsFamiliesUsed.get(currentAspectFamily);
				if (aspectFamilyAuxi==null) {
					aspectFamilyAuxi=new AspectFamily(currentAspectFamily);
					this.aspectsFamiliesUsed.add(aspectFamilyAuxi);
				}
				Vector<String> jpFiles=FilesStore.loadFilesNumberOfCalls();
				for (String jpFileName : jpFiles) {
					if (!this.joinPointsMatched.contains(jpFileName) && !alreadyAdded(jpFileName)) {
						this.joinPointsMatched.add(jpFileName);
						String testCaseFullName=jpFileName.substring(jpFileName.lastIndexOf("/")+1);
						String methodCatchedName=jpFileName.substring(0, jpFileName.lastIndexOf("/"));
						methodCatchedName=methodCatchedName.substring(methodCatchedName.lastIndexOf("/")+1);
						if (methodCatchedName.lastIndexOf(".")!=-1)
							methodCatchedName=methodCatchedName.substring(0, methodCatchedName.lastIndexOf("."));
						aspectFamilyAuxi.addMethodCatched(methodCatchedName);
						aspectFamilyAuxi.addTestCase(methodCatchedName, testCaseFullName);
					}
				}
			}
		}
	}

	protected String loadLaunchingClass(String testCaseName) {
		String launchingClass;
		if (this.executeOnJUnit) {
			launchingClass=this.jUnitFile;
			if (testCaseName!=null) {
				int ini=testCaseName.lastIndexOf("test");
				int fin=testCaseName.indexOf("(");
				String test=testCaseName.substring(ini, fin);
				launchingClass+="#" + test;
			}
		} else {
			launchingClass=this.mainClass;
		}
		return launchingClass;
	}

	protected String loadCurrentClassPath() {
		String currentClassPath="";
		if (this.executionMode!=JFBacterioSecMain.ONLY_ORIGINAL) {
			currentClassPath=this.aspectedOperatorsPath + ":" + this.aspectJRT + ":";
		}
		currentClassPath+=this.jUnitJarPath + ":" + this.workingPath;
		if (this.classPath!=null && this.classPath.length()>0) {
			currentClassPath+=":" + this.classPath;
		}
		return currentClassPath;
	}

	protected Vector<String> loadAspectsToExecute(String aspectedOperatorsPath, String aspectFamily) {
		aspectFamily="/" + aspectFamily;
		Filtro f=new Filtro();
		Vector<String> aspectsClassFiles = f.loadFiles(aspectedOperatorsPath, ".class");

		for (int i=aspectsClassFiles.size()-1; i>=0; i--) {
			String byteStreamFile=aspectsClassFiles.get(i);
			if (byteStreamFile.indexOf("$")!=-1 || byteStreamFile.endsWith("Store.class") || byteStreamFile.endsWith("GenericTracer.class"))
				aspectsClassFiles.remove(i);
		}
		
		if (this.executionMode==JFBacterioSecMain.ONLY_ORIGINAL) {
			aspectsClassFiles.clear();
			aspectsClassFiles.add("");
		}
		
		if (this.executionMode==JFBacterioSecMain.ONLY_COUNT) {
			for (int i=aspectsClassFiles.size()-1; i>=0; i--) {
				String byteStreamFile=aspectsClassFiles.get(i);
				if (byteStreamFile.indexOf("CountCalls")==-1) 
					aspectsClassFiles.remove(i);
			}
		} 

		if (this.executionMode==JFBacterioSecMain.FULL) {
			for (int i=aspectsClassFiles.size()-1; i>=0; i--) {
				String byteStreamFile=aspectsClassFiles.get(i);
				if (!byteStreamFile.startsWith(aspectFamily) || byteStreamFile.indexOf("CountCalls")!=-1)
					aspectsClassFiles.remove(i);
			}
		}
		return aspectsClassFiles;
	}
	
	protected void countCalls() {
		try {
			long timeIni=System.currentTimeMillis();
			execute(this.aspectedOperatorsPath, null, null, null, 0);
			long timeFin=System.currentTimeMillis();
			this.meanTimeInMilliseconds=(timeFin-timeIni);
			ventana.setTimeoutText("Timeout set to " + ((long) (this.meanTimeInMilliseconds*1.10)) + " ms");
			long matches=0;
			
			Enumeration<AspectFamily> aspectsFamilies=this.aspectsFamiliesUsed.getFamilies();
			while (aspectsFamilies.hasMoreElements()) {
				AspectFamily aspectFamily=aspectsFamilies.nextElement();
				ventana.log("Use of " + aspectFamily.getName());
				Enumeration<MethodCatched> methodsCatched = aspectFamily.getMethodsCatched();
				if (!methodsCatched.hasMoreElements())
					ventana.log("\tNot used");
				while (methodsCatched.hasMoreElements()) {
					MethodCatched methodCatched=methodsCatched.nextElement();
					ventana.log("\tUse of " + methodCatched.getName());
					Vector<TestCaseExecuted> ttccee=methodCatched.getTestCases();
					int callsToMethod=0;
					for (TestCaseExecuted tce : ttccee) {
						ventana.log("\t\t" + tce.getName() + "--> " + tce.getNumberOfCalls() + " calls [" + tce.getStartCall() + "-" + tce.getEndCall() + "]");
						callsToMethod+=tce.getNumberOfCalls();
						matches+=tce.getNumberOfCalls();
					}
					if (callsToMethod>0) {
						ventana.addUsedJoinPoint(methodCatched.getName());
					}
				}
			}
			ventana.setMatchesText(matches + " matches; time: " + this.meanTimeInMilliseconds/1000 + " s");
			long timeInSeconds=(matches*this.meanTimeInMilliseconds)/1000;
			String timeText="Estimated execution time: " + timeInSeconds + " s";
			boolean brackets=false;
			String time="";
			if (timeInSeconds>86400) {
				long days=timeInSeconds/86400;
				time=days + " d ";
				timeInSeconds=timeInSeconds%86400;
				brackets=true;
			}
			if (timeInSeconds>3600) {
				long hours=timeInSeconds/3600;
				time+=hours + " h ";
				timeInSeconds=timeInSeconds%3600;
				brackets=true;
			}
			if (timeInSeconds>60) {
				long minutes=timeInSeconds/60;
				time+=minutes + " m ";
				timeInSeconds=timeInSeconds%60;
				brackets=true;
			}
			time+=timeInSeconds + "s";
			if (brackets) {
				timeText+=" (" + time + ")";
			}
			ventana.setTimeText(timeText);
		} catch (IOException e) {
			ventana.log(e.toString());
			return;
		}				
	}

	protected void setUp() {
		ventana.setNumberOfTasks(2);
		ventana.setCurrentTaskText("1/2: Cleaning temp path");
		this.aspectsFamiliesUsed.clear();
		this.joinPointsMatched.clear();
		cleanTempPath();
		ventana.updateProgressBarTask();
		
		ventana.setCurrentTaskText("2/2: Copying Store.class");
		File file=new File(".");
		this.aspectedOperatorsPath+=this.aspectedOperatorsPath.endsWith("/") ? "" : "/";
		String newStoreFileName=file.getAbsolutePath() + "/bin/edu/uclmgmu/bacteriosec/aspects/common/Store.class";
		try {
			// TODO: recuperar
			//JFBacterioSecMain.copyFile(this.aspectedOperatorsPath + "edu/uclmgmu/bacteriosec/aspects/common/Store.class", newStoreFileName);
			ventana.updateProgressBarTask();
		} catch (Exception e) {
			ventana.log(e.toString());
			ventana.updateProgressBarTask();
			return;
		}		
	}
	
	protected void createResultsDirectory() {
		String tempPath=System.getProperty("java.io.tmpdir");
		tempPath+="bacterioSecResults/";
		(new File(tempPath)).mkdir();
		Calendar c=Calendar.getInstance();
		String directory=c.get(Calendar.YEAR) + "-" + c.get(Calendar.MONTH) + "-" + c.get(Calendar.DATE) + "_" + 
							c.get(Calendar.HOUR_OF_DAY) + "." + c.get(Calendar.MINUTE) + "." + c.get(Calendar.SECOND) + "/";
		this.resultsPath=tempPath+directory;
		(new File(this.resultsPath)).mkdir();
		this.ventana.setResultsPath(this.resultsPath);
	}
	
	private void cleanTempPath() {
		String tempPath=System.getProperty("java.io.tmpdir");
		(new File(tempPath + "callInTurn.txt")).delete();
		(new File(tempPath + "currentCall.txt")).delete();
		FilesStore.cleanTempPath();
		this.alreadyAddeds.clear();
	}

	private boolean alreadyAdded(String jpFileName) {
		if (!this.alreadyAddeds.contains(jpFileName)) {
			this.alreadyAddeds.add(jpFileName);
			return false;
		}
		return true;
	}
	
	protected void makeATrace() {
		String byteStreamFile="edu/uclmgmu/bacteriosec/aspects/common/GenericTracer";
		String aopXMLFilePath=this.workingPath;
		aopXMLFilePath+=(aopXMLFilePath.endsWith("/") ? "" : "/");
		updateAOPFile(byteStreamFile);
		String classPath=this.aspectedOperatorsPath + ":" + this.aspectJRT + ":" + this.jUnitJarPath + ":" + this.workingPath;
		if (this.classPath!=null && this.classPath.length()>0) {
			classPath+=":" + this.classPath;
		}
		boolean usingJUnit=this.executeOnJUnit;
		String launchingClass=usingJUnit ? this.jUnitFile : this.mainClass;
		long timeOut=0;
		Executor e=new Executor(ventana, this.aspectJWeaver, classPath, launchingClass, ventana.getBootPath(), ventana.getWorkingPath(), timeOut);
		String[] salida={""};
		e.setOutputText(salida);
		e.setUsingJUnit(usingJUnit);
		e.run();
		int rt=e.getReturnValue();	
		ventana.log(byteStreamFile, rt, salida[0]);
	}
	
	protected String updateAOPFile(String aspectClassFile) {
		String aopXMLFilePath=this.workingPath;
		aopXMLFilePath+=(aopXMLFilePath.endsWith("/") ? "" : "/");
		if (this.executionMode==JFBacterioSecMain.ONLY_ORIGINAL) {
			this.deleteAOPFile(aopXMLFilePath);
		} else {
			aspectClassFile=(aspectClassFile.startsWith("/") ? "" : "/") + aspectClassFile;
			try {
				if (aspectClassFile.endsWith(".class"))
					aspectClassFile=aspectClassFile.substring(0, aspectClassFile.length()-6);
				if (aspectClassFile.startsWith("/"))
					aspectClassFile=aspectClassFile.substring(1);
				String text="<aspectj><aspects>";
				text+="<aspect name=\"" + aspectClassFile + "\"/>";
				//text+="<aspect name=\"com.example.callgraph.CallInterceptor\"/>";
				text+="</aspects></aspectj>";
				aopXMLFilePath+="META-INF/aop-ajc.xml";
				FilesStore.saveAOP_AJC_XML(aopXMLFilePath, text);
				System.out.println(aspectClassFile);
			} catch (IOException e) {
				ventana.log(e.toString());
			}
		}
		return aspectClassFile;
	}
	
	private void deleteAOPFile(String aopXMLPath) {
		new File(aopXMLPath + "/META-INF/aop-ajc.xml").delete();
	}

	public void removeMatchedJoinPoint(String jpName) {
		this.aspectsFamiliesUsed.removeMethodCatched(jpName);
	}
}
